Skip to content

Add [DynamoDBEvent] annotation attribute and source generator support#2320

Draft
GarrettBeatty wants to merge 1 commit intodevfrom
feature/dynamodb-annotations
Draft

Add [DynamoDBEvent] annotation attribute and source generator support#2320
GarrettBeatty wants to merge 1 commit intodevfrom
feature/dynamodb-annotations

Conversation

@GarrettBeatty
Copy link
Copy Markdown
Collaborator

@GarrettBeatty GarrettBeatty commented Apr 3, 2026

Summary

Adds [DynamoDBEvent] annotation attribute support to the Lambda Annotations framework, enabling developers to declaratively configure DynamoDB stream-triggered Lambda functions directly in C# code. The source generator automatically produces the corresponding SAM/CloudFormation template configuration at build time.

User Experience

With this change, developers can write DynamoDB stream-triggered Lambda functions like this:

[LambdaFunction]
[DynamoDBEvent("@MyTable")]
public async Task ProcessRecords(DynamoDBEvent evnt)
{
    foreach (var record in evnt.Records)
    {
        // Handle stream records
    }
}

The source generator will automatically generate the SAM template entry:

ProcessRecords:
  Type: AWS::Serverless::Function
  Properties:
    Events:
      MyTable:
        Type: DynamoDB
        Properties:
          Stream: !GetAtt MyTable.StreamArn
          StartingPosition: LATEST
          BatchSize: 100

Attribute Properties

Property Required Description Default
Stream Yes DynamoDB stream reference (@TableName for CloudFormation Ref, or stream ARN) -
ResourceName No CloudFormation event resource name Derived from stream/table name
BatchSize No Max records per batch (1-10000) 100
StartingPosition No LATEST or TRIM_HORIZON LATEST
MaximumBatchingWindowInSeconds No Max time to gather records (0-300) Not set
Filters No Semicolon-separated filter patterns Not set
Enabled No Whether the event source is enabled true

Compile-Time Validation

The source generator validates at build time:

  • Stream reference: Must start with @ (CloudFormation resource) or be a valid DynamoDB stream ARN
  • Method signature: First parameter must be DynamoDBEvent, optional second parameter must be ILambdaContext
  • Return type: Must be void or Task
  • Dependencies: Project must reference Amazon.Lambda.DynamoDBEvents NuGet package
  • BatchSize: Must be between 1 and 10000
  • MaximumBatchingWindowInSeconds: Must be between 0 and 300
  • StartingPosition: Must be LATEST or TRIM_HORIZON
  • Resource name: Must be alphanumeric if explicitly set

Example with all properties

[LambdaFunction]
[DynamoDBEvent("@MyTable",
    ResourceName = "TableStreamEvent",
    BatchSize = 50,
    StartingPosition = "TRIM_HORIZON",
    MaximumBatchingWindowInSeconds = 60,
    Filters = "{\"eventName\": [\"INSERT\"]}",
    Enabled = true)]
public async Task ProcessRecords(DynamoDBEvent evnt, ILambdaContext context)
{
    context.Logger.LogLine($"Processing {evnt.Records.Count} records");
}

What Changed

Annotation Attribute (Amazon.Lambda.Annotations)

  • New DynamoDBEventAttribute class in Amazon.Lambda.Annotations.DynamoDB namespace with configurable properties and built-in validation

Source Generator (Amazon.Lambda.Annotations.SourceGenerator)

  • DynamoDBEventAttributeBuilder — extracts attribute data from Roslyn syntax tree
  • AttributeModelBuilder — recognizes and routes DynamoDBEvent attributes
  • EventTypeBuilder — maps to EventType.DynamoDB
  • SyntaxReceiver — registers DynamoDBEvent as a recognized attribute
  • TypeFullNames — adds DynamoDB type constants
  • LambdaFunctionValidator — validates method signatures, return types, dependencies, and attribute properties
  • CloudFormationWriter.ProcessDynamoDBAttribute() — generates SAM template with Stream (GetAtt StreamArn or ARN), StartingPosition, BatchSize, MaximumBatchingWindowInSeconds, FilterCriteria, and Enabled
  • New diagnostic AWSLambda0132 for invalid DynamoDBEventAttribute errors

Tests

  • Attribute unit tests covering constructor, defaults, property tracking, and validation
  • CloudFormation writer tests covering attribute configurations and template formats
  • E2E source generator snapshot tests (sync + async)
  • Integration test deploying a real DynamoDB stream-triggered Lambda and verifying event source mapping

Related: DOTNET-8571

@GarrettBeatty GarrettBeatty force-pushed the feature/dynamodb-annotations branch 2 times, most recently from d022bf4 to 4e6ca78 Compare April 13, 2026 17:31
- DynamoDBEventAttribute with Stream, ResourceName, BatchSize, StartingPosition, MaximumBatchingWindowInSeconds, Filters, Enabled
- DynamoDBEventAttributeBuilder for Roslyn AttributeData parsing
- Source generator wiring (TypeFullNames, SyntaxReceiver, EventTypeBuilder, AttributeModelBuilder)
- CloudFormationWriter ProcessDynamoDBAttribute (SAM DynamoDB event source mapping)
- LambdaFunctionValidator ValidateDynamoDBEvents
- DiagnosticDescriptors InvalidDynamoDBEventAttribute (AWSLambda0132)
- DynamoDBEventAttributeTests (attribute unit tests)
- DynamoDBEventsTests (CloudFormation writer tests)
- E2E source generator snapshot tests
- Integration test (DynamoDBEventSourceMapping)
- Sample function (DynamoDbStreamProcessing)
- .autover change file
- README documentation
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds DynamoDB Streams trigger support to Lambda Annotations via a new [DynamoDBEvent] attribute and extends the source generator to validate usage and emit the corresponding SAM/CloudFormation DynamoDB event configuration.

Changes:

  • Introduces DynamoDBEventAttribute (including validation and derived resource naming) in Amazon.Lambda.Annotations.DynamoDB.
  • Extends the source generator to recognize/build/validate DynamoDB events and write the SAM template event mapping.
  • Adds unit, snapshot, and integration tests plus a test serverless app DynamoDB table and handler.

Reviewed changes

Copilot reviewed 27 out of 27 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
Libraries/test/TestServerlessApp/serverless.template Adds a DynamoDB table with Streams enabled for integration testing.
Libraries/test/TestServerlessApp/TestServerlessApp.csproj References Amazon.Lambda.DynamoDBEvents for the test app.
Libraries/test/TestServerlessApp/DynamoDbStreamProcessing.cs Adds a sample DynamoDB stream Lambda handler using the new attribute.
Libraries/test/TestServerlessApp/DynamoDBEventExamples/ValidDynamoDBEvents.cs.txt Adds valid attribute usage inputs for source generator snapshot tests.
Libraries/test/TestServerlessApp.IntegrationTests/TestServerlessApp.IntegrationTests.csproj Adds DynamoDB SDK dependency for integration validation.
Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs Fetches DynamoDB Stream ARN from the deployed test table; updates expected function count.
Libraries/test/TestServerlessApp.IntegrationTests/DynamoDBEventSourceMapping.cs New integration test validating the deployed event source mapping config.
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/DynamoDBEventsTests.cs Adds CloudFormation writer tests for DynamoDB event rendering and sync behavior.
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs Adds snapshot-based source generator test coverage for DynamoDB events.
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/dynamoDBEvents.template Snapshot of generated template containing DynamoDB events.
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/DynamoDB/ValidDynamoDBEvents_ProcessMessages_Generated.g.cs Snapshot of generated handler for sync DynamoDB event usage.
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/DynamoDB/ValidDynamoDBEvents_ProcessMessagesAsync_Generated.g.cs Snapshot of generated handler for async DynamoDB event usage.
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/DynamoDBEventAttributeTests.cs Adds unit tests for attribute defaults, tracking, and validation.
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/CSharpSourceGeneratorVerifier.cs Adds DynamoDBEvents assembly reference to generator test harness.
Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj Adds project reference to Amazon.Lambda.DynamoDBEvents for tests.
Libraries/src/Amazon.Lambda.Annotations/README.md Documents the new DynamoDB event attribute and usage.
Libraries/src/Amazon.Lambda.Annotations/DynamoDB/DynamoDBEventAttribute.cs Implements the new [DynamoDBEvent] attribute and validation logic.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs Emits DynamoDB event definitions into the generated template.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs Adds dependency + signature + attribute-property validation for DynamoDB events.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs Adds DynamoDB type/attribute full-name constants and recognized attribute list entry.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs Registers DynamoDBEventAttribute as a recognized annotation.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs Adds mapping from DynamoDBEventAttribute to EventType.DynamoDB.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/DynamoDBEventAttributeBuilder.cs Builds DynamoDBEventAttribute model from Roslyn AttributeData.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs Routes DynamoDBEventAttribute to the new builder/model.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs Adds a new diagnostic descriptor for invalid DynamoDBEventAttribute.
Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md Records the newly added diagnostic in analyzer release notes.
.autover/changes/add-dynamodbevent-annotation.json Adds an autover change entry for the new feature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}
if (!Stream.StartsWith("@"))
{
if (!Stream.Contains(":dynamodb:") && !Stream.Contains("/stream/"))
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validate() treats a stream ARN as valid if it contains either ":dynamodb:" or "/stream/" because the condition uses &&. This will let invalid ARNs through (e.g., missing /stream/). Update validation to require both parts (or parse the ARN structure similarly to SQSEventAttribute.Validate()).

Suggested change
if (!Stream.Contains(":dynamodb:") && !Stream.Contains("/stream/"))
if (!Stream.Contains(":dynamodb:") || !Stream.Contains("/stream/"))

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +36
if (Stream.StartsWith("@"))
{
return Stream.Substring(1);
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stream can be null (attributes can be invoked with null string constants), but ResourceName/Validate() call Stream.StartsWith(...) unconditionally. This can throw and crash the generator. Add a null/empty check (and return a validation error) before dereferencing Stream in ResourceName and Validate() (and consider rejecting "@" with no resource name).

Suggested change
if (Stream.StartsWith("@"))
{
return Stream.Substring(1);
if (string.IsNullOrWhiteSpace(Stream))
{
return string.Empty;
}
if (Stream.StartsWith("@"))
{
return Stream.Length > 1 ? Stream.Substring(1) : string.Empty;

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +64
/// <summary>
/// The maximum number of records in each batch that Lambda pulls from the stream.
/// Default value is 100.
/// </summary>
public uint BatchSize
{
get => batchSize.GetValueOrDefault();
set => batchSize = value;
}
private uint? batchSize { get; set; }
internal bool IsBatchSizeSet => batchSize.HasValue;
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BatchSize XML comment says the default is 100, but when unset the property returns 0 (because GetValueOrDefault() is used). Either initialize batchSize to 100, change the getter to GetValueOrDefault(100), or update the comment to clarify that the property is omitted from the template unless explicitly set and SAM/Lambda applies the default.

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +83
/// <summary>
/// If set to false, the event source mapping will be disabled. Default value is true.
/// </summary>
public bool Enabled
{
get => enabled.GetValueOrDefault();
set => enabled = value;
}
private bool? enabled { get; set; }
internal bool IsEnabledSet => enabled.HasValue;

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Enabled XML comment says the default is true, but when unset the property returns false (GetValueOrDefault()). This is surprising if user code reflects over the attribute, and it also conflicts with the stated default. Consider using GetValueOrDefault(true) or clarifying in the comment that the property is omitted from the generated template unless explicitly set (so the AWS default applies).

Copilot uses AI. Check for mistakes.
Comment on lines +122 to +125
if (!string.IsNullOrEmpty(StartingPosition) && StartingPosition != "TRIM_HORIZON" && StartingPosition != "LATEST")
{
validationErrors.Add($"{nameof(DynamoDBEventAttribute.StartingPosition)} = {StartingPosition}. It must be either TRIM_HORIZON or LATEST");
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validate() allows StartingPosition to be null/empty (no error), but CloudFormationWriter always writes StartingPosition to the template. If a user sets StartingPosition = null, the generated SAM template will contain an invalid null value. Treat null/empty as invalid or coerce it back to the default (LATEST) during validation/building.

Copilot uses AI. Check for mistakes.
<ItemGroup>
<!-- AWSSDK.SecurityToken is needed at runtime for environments which uses assume-role operation for credentials -->
<PackageReference Include="AWSSDK.SecurityToken" Version="3.7.1.99" />
<PackageReference Include="AWSSDK.DynamoDBv2" Version="3.7.*" />
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The integration test project uses a floating NuGet version (3.7.*) for AWSSDK.DynamoDBv2, which can make CI builds non-reproducible and introduce unexpected behavior when a new patch is released. Pin this to a specific version (consistent with AWSSDK.SecurityToken in the same file).

Suggested change
<PackageReference Include="AWSSDK.DynamoDBv2" Version="3.7.*" />
<PackageReference Include="AWSSDK.DynamoDBv2" Version="3.7.1.99" />

Copilot uses AI. Check for mistakes.
Console.WriteLine($"[IntegrationTest] TestTable: {testTableName}");
if (!string.IsNullOrEmpty(testTableName))
{
var dynamoDbClient = new Amazon.DynamoDBv2.AmazonDynamoDBClient(Amazon.RegionEndpoint.USWest2);
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AmazonDynamoDBClient is created but never disposed. Even in tests, it's better to dispose AWS SDK clients (or reuse a shared client) to avoid socket exhaustion/resource leaks across test runs; consider using var dynamoDbClient = ... or keeping a single client on the fixture like the other helpers.

Suggested change
var dynamoDbClient = new Amazon.DynamoDBv2.AmazonDynamoDBClient(Amazon.RegionEndpoint.USWest2);
using var dynamoDbClient = new Amazon.DynamoDBv2.AmazonDynamoDBClient(Amazon.RegionEndpoint.USWest2);

Copilot uses AI. Check for mistakes.
Comment on lines +285 to +290
public static readonly DiagnosticDescriptor InvalidDynamoDBEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0137",
title: "Invalid DynamoDBEventAttribute",
messageFormat: "Invalid DynamoDBEventAttribute encountered: {0}",
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions a new diagnostic AWSLambda0132 for invalid DynamoDBEventAttribute, but in code the new diagnostic is AWSLambda0137 (and AWSLambda0132 is already used for Invalid ALBApiAttribute). Update the PR description (or the diagnostic ID, if the description is authoritative) to avoid confusion for users.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants